﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.ComponentModel;
using ScrumTable.BL.DM.DataManagement.Logging;
using ScrumTable.BL.DM.DataManagement.Tracking.EditCommands;
using ScrumTable.BL.DM.Properties;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.EventHandling;
using ScrumTable.Common.Threading;
using ScrumTable.DL.Data.Common;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.Schema.ScrumTableV1;
using System.Collections.Generic;

#endregion

namespace ScrumTable.BL.DM.DataManagement
{
    /// <summary>
    /// This abstract class specifies the base class for all domain data
    /// objects and collections.
    /// </summary>
    public abstract class DomainBase : DisposeBase, IEquatable<DomainBase>, INotifyPropertyChanged, INotifyPropertyChanging
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const string BackEndIdPropertyName = "BackEndId";
        private const string IsSynchronizedPropertyName = "IsSynchronized";

        private const string EvtChanging = "OnChanging";
        private const string EvtChanged = "OnChanged";

        private readonly DelegateHandlerCollection<PropertyChangedEventHandler> _changedEvents = new DelegateHandlerCollection<PropertyChangedEventHandler>();
        private readonly DelegateHandlerCollection<PropertyChangingEventHandler> _changingEvents = new DelegateHandlerCollection<PropertyChangingEventHandler>();
        private readonly IDictionary<string, IDisposable> _stateHandler = new Dictionary<string, IDisposable>();

        private DomainBase _parent;
        private IDataObject _dataObject;
        private UpdateState _updateState = UpdateState.None;

        private bool _isSynchronized = true;
        private IList<DomainSyncErrorInfo> _synchronizationErrors;
        private bool _hasSynchronizationErrors;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns true if the current instance contains synchronization errors.
        /// </summary>
        public bool HasSynchronizationErrors
        {
            get { return _hasSynchronizationErrors; }
            private set
            {
                if (_hasSynchronizationErrors != value)
                {
                    FirePropertyChanging("HasSynchronizationErrors");
                    _hasSynchronizationErrors = value;
                    FirePropertyChanged("HasSynchronizationErrors");
                }
            }
        }

        /// <summary>
        /// Gets the list of errors occurred during the synchronization process.
        /// </summary>
        public IList<DomainSyncErrorInfo> SynchronizationErrors
        {
            get { return _synchronizationErrors; }
            private set
            {
                if (_synchronizationErrors != value)
                {
                    FirePropertyChanging("SynchronizationErrors");
                    _synchronizationErrors = value;
                    HasSynchronizationErrors = (value != null);
                    FirePropertyChanged("SynchronizationErrors");
                }
            }
        }

        /// <summary>
        /// Gets the id of the current domain object. Its content differs
        /// from the underlying system and may contain a unique name (for AD
        /// objects) or an identification number.
        /// </summary>
        public string Id { get; private set; }

        /// <summary>
        /// Gets the unique id of the underlying back end system identity.
        /// </summary>
        public virtual string BackEndId { get; protected set; }

        /// <summary>
        /// Gets true if the changes have been synchronized with the back-end system.
        /// </summary>
        public virtual bool IsSynchronized
        {
            get { return _isSynchronized; }
            protected set
            {
                _isSynchronized = value;
                if (value) { SynchronizationErrors = null; }
            }
        }

        /// <summary>
        /// Gets the name of the current domain object. In most cases this
        /// property will contain the domain object title.
        /// </summary>
        public virtual string Name
        {
            get { return Id; }
            set { }
        }

        /// <summary>
        /// Gets the parent instance of the current domain object.
        /// </summary>
        public DomainBase Parent
        {
            get
            {
                EnsureNotDisposed();
                return _parent;
            }
            internal set
            {
                EnsureNotDisposed();

                bool fireEvents = FireParentChangedEvent
                    && _parent != null
                    && _parent != value
                    && !_parent.Equals(value);

                if (fireEvents) { FirePropertyChanging("Parent"); }
                _parent = value;
                if (fireEvents) { FirePropertyChanged("Parent"); }
            }
        }

        /// <summary>
        /// Sets a boolean which indicates that the Parent property changed should
        /// be fired.
        /// </summary>
        public bool FireParentChangedEvent
        {
            get; set;
        }

        /// <summary>
        /// Gets the domain context which provides information about the object hierarchy.
        /// </summary>
        public IDomainContext Context
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the key/type  chain with the current domain object.
        /// </summary>
        public ValueChain<Pair<string, Type>> TypedIdChain
        {
            get
            {
                Pair<string, Type> typeIdPair = new Pair<string, Type>(Id, GetType());

                if (Parent == null)
                    return new ValueChain<Pair<string, Type>>(typeIdPair);
                return new ValueChain<Pair<string, Type>>(Parent.TypedIdChain, typeIdPair);
            }
        }

        /// <summary>
        /// Gets the key chain of the current domain base object.
        /// </summary>
        public ValueChain<string> IdChain
        {
            get
            {
                if (Parent == null)
                    return new ValueChain<string>(Id);
                return new ValueChain<string>(Parent.IdChain, Id);
            }
        }

        /// <summary>
        /// Gets/sets the data object associated with the current element.
        /// </summary>
        internal IDataObject DataObject
        {
            get
            {
                EnsureNotDisposed();

                if (_updateState != UpdateState.None)
                    throw new InvalidOperationException(Resources.ExcDmnObjAsyncCallbackPending);

                return _dataObject;
            }
            set
            {
                PreCondition.AssertNotNull(value, "value");
                EnsureNotDisposed();
                DisposeDataObject();

                _dataObject = value;
                _dataObject.PropertyChanged += OnDataPropertyChanged;
                _dataObject.PropertyChanging += OnDataPropertyChanging;
                
                Id = _dataObject.Id;
                HashCode = Id.GetHashCode();

                BackEndId = _dataObject.BackEndId;
                IsSynchronized = _dataObject.IsSynchronized;
            }
        }

        /// <summary>
        /// Gets the key chain of the current domain object.
        /// </summary>
        internal protected ValueChain<string> ParentIdChain
        {
            get
            {
                if (Parent == null)
                    return new ValueChain<string>();
                return Parent.IdChain;
            }
        }

        /// <summary>
        /// Gets the parent collection instance.
        /// </summary>
        internal abstract IDomainCollection ParentCollection { get; }

        /// <summary>
        /// Gets a collection which contains all child collections
        /// (IDomainCollection instances) of the current domain object.
        /// </summary>
        internal abstract ICollection ChildCollections { get; }

        /// <summary>
        /// Gets the internal domain context.
        /// </summary>
        internal DomainContext ContextInternal { get { return (DomainContext)Context; } }

        /// <summary>
        /// Gets the parent iteration object. If the current instance is
        /// an iteration, you will the current object.
        /// </summary>
        protected Iteration ParentIteration
        {
            get { return SearchParent<Iteration>(); }
        }

        /// <summary>
        /// Gets the parent project object. If the current instance is
        /// a project, you will the current object.
        /// </summary>
        protected Project ParentProject
        {
            get { return SearchParent<Project>(); }
        }

        /// <summary>
        /// Gets the associated data layer schema.
        /// </summary>
        protected ScrumTableV1Schema Schema
        {
            get
            {
                EnsureNotDisposed();
                return ContextInternal.Schema;
            }
        }

        /// <summary>
        /// Gets the domain object schema of the current domain object.
        /// </summary>
        protected abstract SchemaBase DomainSchema { get; }

        /// <summary>
        /// Gets an instance which contains mappings between the data layer
        /// schema and the business layer.
        /// </summary>
        protected DomainSchemaMapper PropertySchemaMappings { get; private set; }

        /// <summary>
        /// Gets an instance which contains mappings between the data layer
        /// schema and the business layer.
        /// </summary>
        protected DomainPropertyMapper PropertyObjectMappings { get; private set; }

        private int HashCode { get; set; }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DomainBase instance. This constructor
        /// is protected because its needed by derived classes only.
        /// </summary>
        /// <remarks>
        /// After calling this constructor, you have to set the Context and
        /// Parent properties manually.
        /// </remarks>
        protected DomainBase()
        {
            PropertySchemaMappings = new DomainSchemaMapper();
            PropertyObjectMappings = new DomainPropertyMapper();
            FireParentChangedEvent = false;

            Id = GetType().Name;
            _isSynchronized = true;
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// This method represents a simple factory for all Domain elements
        /// which are derived from DomainBase.
        /// </summary>
        /// <typeparam name="T">Specifies the type of the DomainBase element to instantiate.</typeparam>
        /// <param name="domainContext">Specifies the assigned domain context (as owner).</param>
        /// <param name="dataObject">Specifies the underlying data object.</param>
        /// <param name="parent">Specifies the parent domain base object.</param>
        /// <returns>Returns the created instance.</returns>
        internal static T CreateInstance<T>(IDomainContext domainContext, IDataObject dataObject, DomainBase parent)
            where T : DomainBase, new()
        {
            PreCondition.AssertNotNull(domainContext, "domainContext");
            PreCondition.AssertNotNull(dataObject, "dataObject");

            T instance = new T
            {
                Context = domainContext,
                Parent = parent,
                DataObject = dataObject
            };
            
            instance.InitializeMappings();
            return instance;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">Specifies the left object.</param>
        /// <param name="right">Specifies the right object.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(DomainBase left, DomainBase right)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(left, right))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if ((object)left == null || (object)right == null)
            {
                return false;
            }

            // Return true if the fields match:
            return left.Equals(right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">Specifies the left object.</param>
        /// <param name="right">Specifies the right object.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(DomainBase left, DomainBase right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Determines whether the specified IDataObject is equal to the current object.
        /// </summary>
        /// <param name="toCompare">Parameter to check.</param>
        /// <returns>Returns true if the objects are equal.</returns>
        public override bool Equals(object toCompare)
        {
            // If parameter cannot be cast to DomainBase return false.
            return Equals(toCompare as DomainBase);
        }

        /// <summary>
        /// Determines whether the specified IDataObject is equal to the current object.
        /// </summary>
        /// <param name="obj">Parameter to check.</param>
        /// <returns>Returns true if the objects are equal.</returns>
        public bool Equals(DomainBase obj)
        {
            // If parameter is null return false:
            if ((object)obj == null) // force compiler to use Object::operator==()
                return false;

            // Return true if the fields match:
            return (obj.Id == Id);
        }

        /// <summary>
        /// Returns the hash code for this IDataObject.
        /// </summary>
        /// <returns>Returns the computed hash value.</returns>
        public override int GetHashCode()
        {
            return HashCode;
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format("{0} [{1}]", base.ToString(), Id);
        }

        /// <summary>
        /// Stores the current domain object back into the data layer.
        /// </summary>
        public void Save()
        {
            ActionLogger.InfoFormat(Resources.LogActDmnBaseSave, Name, IdChain);

            if (GetType() != ParentCollection.ItemType)
            {
                throw new InvalidOperationException(string.Format(
                    Resources.LogDmnBaseTypeInconsistency,
                    (Parent != null) ? Parent.GetType() : null,
                    (Parent != null) ? Parent.IdChain : null,
                    GetType(),
                    IdChain,
                    DomainSchema,
                    Schema));
            }
            ContextInternal.Data.Storage[DomainSchema].Update(ParentIdChain, DataObject);
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object wiht the
        /// given id/type chain and returns the found element.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <returns>Returns the retrieved element.</returns>
        public DomainBase TryGetByChain(ValueChain<Pair<string, Type>> typedIdChain)
        {
            EnsureNotDisposed();
            Pair<string, Type> elementChain = typedIdChain.Bottom;

            foreach (IDomainCollection domainCollection in ChildCollections)
            {
                if (domainCollection.ItemType != elementChain.Right)
                    continue;

                DomainBase domainObject = domainCollection.TryGetById(elementChain.Left);

                if (domainObject != null && domainObject.GetType() == elementChain.Right)
                {
                    ValueChain<Pair<string, Type>> subChain = typedIdChain.SubChain(1);

                    if (!subChain.IsEmpty)
                        return domainObject.TryGetByChain(subChain);
                    return domainObject;
                }
            }
            return null;
        }


        /// <summary>
        /// Loads the associated (external) data of the current instance. This will
        /// release older values and reinitialize them.
        /// </summary>
        /// <param name="deep">True to load all sub objects.</param>
        public void LoadData(bool deep)
        {
            ActionLogger.InfoFormat(Resources.LogActDmnBaseLoadDataSync, Name, IdChain, deep);

            EnsureNotDisposed();
            PrepareDataLoad();
            DataObject = ContextInternal.Data.Storage[DomainSchema].GetById(ParentIdChain, Id);

            if (deep)
                LoadChildData();
        }

        /// <summary>
        /// Loads the associated (external) data of the current instance. This will
        /// release older values and reinitialize them.
        /// </summary>
        /// <param name="deep">True to load all sub objects.</param>
        /// <param name="callback">Specifies the asynchronous callback method. The callback value is of type DataResult.</param>
        public void LoadDataAsync(bool deep, AsyncCallback callback)
        {
            PreCondition.AssertNotNull(callback, "callback");
            ActionLogger.InfoFormat(Resources.LogActDmnBaseLoadDataASync, Name, IdChain, deep);

            EnsureNotDisposed();
            PrepareDataLoad();
            _updateState = UpdateState.CallbackPending;

            AsyncCallbackHandler asyncHandler = new AsyncCallbackHandler(
                this,
                result =>
                {
                    _updateState = UpdateState.None;
                    callback(result);
                },
                DomainSchema,
                ChildCollections);

            if (deep)
                asyncHandler.InitDeep();
            else
                asyncHandler.InitFlat();
        }

        /// <summary>
        /// Searches in the domain context tree for a domain object wiht the
        /// given id chain and returns the found element. This procedure is slower
        /// than the ValueChain&gt;Pair&gt;string, Type&lt;&lt; variant and not type safe.
        /// </summary>
        /// <param name="typedIdChain">Specifies the id chain of the element to retrieve.</param>
        /// <returns>Returns the retrieved element.</returns>
        internal DomainBase TryGetByChain(ValueChain<string> typedIdChain)
        {
            EnsureNotDisposed();
            string elementId = typedIdChain.Bottom;

            foreach (IDomainCollection domainCollection in ChildCollections)
            {
                DomainBase domainObject = domainCollection.TryGetById(elementId);

                if (domainObject != null)
                {
                    ValueChain<string> subChain = typedIdChain.SubChain(1);

                    if (!subChain.IsEmpty)
                        return domainObject.TryGetByChain(subChain);
                    return domainObject;
                }
            }
            return null;
        }

        /// <summary>
        /// Loads the associated (external) data of the current child instances. This will
        /// release older values and reinitialize them.
        /// </summary>
        /// <param name="callback">Specifies the asynchronous callback method. The callback value is of type DataResult.</param>
        internal void LoadChildDataAsyncDeep(AsyncCallback callback)
        {
            EnsureNotDisposed();
            LoadChildDataAsync(true, callback);
        }

        /// <summary>
        /// Searches for a child collection which contains the given child item type.
        /// </summary>
        /// <typeparam name="T">Specifies the item type of the child collection.</typeparam>
        /// <returns>Returns the retrieved domain collection with the given child items.</returns>
        internal IDomainCollection SearchCollection<T>()
            where T : DomainBase, new()
        {
            foreach (IDomainCollection childCollection in ChildCollections)
            {
                if (childCollection.ItemType == typeof(T))
                    return childCollection;
            }

            throw new InvalidOperationException(string.Format(
                Resources.ExcDmnObjInvalidParentCol,
                GetType().Name,
                typeof(T).Name));
        }

        /// <summary>
        /// Resets all child collections (IDomainCollection instances) of the
        /// current domain object.
        /// </summary>
        protected abstract void ResetChildCollections();

        /// <summary>
        /// Loads the associated (external) data of the current child instances. This will
        /// release older values and reinitialize them.
        /// </summary>
        /// <param name="callback">Specifies the asynchronous callback method. The callback value is of type DataResult.</param>
        /// <param name="deep">Specifies the asynchronous callback method. The callback value is of type DataResult.</param>
        protected void LoadChildDataAsync(bool deep, AsyncCallback callback)
        {
            PreCondition.AssertNotNull(callback, "callback");

            PrepareDataLoad();
            _updateState = UpdateState.CallbackPending;

            AsyncCallbackHandler asyncHandler = new AsyncCallbackHandler(
                this,
                result =>
                    {
                        _updateState = UpdateState.None;
                        callback(result);
                    },
                DomainSchema,
                ChildCollections);

            if (deep)
                asyncHandler.InitDeepWithoutCurrent();
            else
                asyncHandler.InitFlatWithoutCurrent();
        }

        /// <summary>
        /// Loads the associated (external) data of the current child instances. This will
        /// release older values and reinitialize them.
        /// </summary>
        internal void LoadChildData()
        {
            PrepareDataLoad();

            foreach (IDomainCollection collection in ChildCollections)
            {
                collection.LoadData(true);
            }
        }

        /// <summary>
        /// Notifies the current instance about a synchronization error and returns
        /// additional error information about the specified error.
        /// </summary>
        /// <param name="errorInfoList">Specifies the error list provided by the underlying data layer.</param>
        /// <returns>Returns a new DomainSyncErrorInfo instance with additional error information.</returns>
        internal void NotifySynchronizationErrors(IList<IDataSyncErrorInfo> errorInfoList)
        {
            PreCondition.AssertNotNull(errorInfoList, "errorInfoList");

            SynchronizationErrors = errorInfoList.Convert(CreateDomainSyncErrorInfo);
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">Name of the changed property.</param>
        internal void FirePropertyChanged(string propertyName)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                OnPropertyChanged(new DomainChangedEventArgs(this, propertyName));
            }
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="propertyName">Name of the changing property.</param>
        internal void FirePropertyChanging(string propertyName)
        {
            if (!string.IsNullOrEmpty(propertyName))
            {
                OnPropertyChanging(new DomainChangingEventArgs(this, propertyName));
            }
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="schema">Schema of the changing property.</param>
        protected void FirePropertyChanged(SchemaBase schema)
        {
            PreCondition.AssertNotNull(schema, "schema");

            OnPropertyChanged(new DomainChangedEventArgs(this, PropertySchemaMappings[schema]));
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="schema">Schema of the changing property.</param>
        protected void FirePropertyChanging(SchemaBase schema)
        {
            PreCondition.AssertNotNull(schema, "schema");

            OnPropertyChanging(new DomainChangingEventArgs(this, PropertySchemaMappings[schema]));
        }

        /// <summary>
        /// Creates a new undo state entry and adds it to the undo manager
        /// during the Dispose() call of the returnig disposable object.
        /// 
        /// This method is used to track property value changes on a
        /// domain object.
        /// </summary>
        protected void SetDataWithUndoAction(SchemaBase schema, object value)
        {
            PreCondition.AssertNotNull(schema, "schema");

            using (new UndoStateHandler(this, schema, PropertySchemaMappings.CanUndo))
            {
                DataObject.SetData(
                    schema,
                    value,
                    () => FirePropertyChanging(schema),
                    () => FirePropertyChanged(schema));
            }
        }

        /// <summary>
        /// Creates a new undo state entry and adds it to the undo manager
        /// during the Dispose() call of the returnig disposable object.
        /// 
        /// This method is used to track property value changes on a
        /// domain object.
        /// </summary>
        protected void SetDataNotNullWithUndoAction(SchemaBase schema, object value)
        {
            PreCondition.AssertNotNull(schema, "schema");

            using (new UndoStateHandler(this, schema, PropertySchemaMappings.CanUndo))
            {
                DataObject.SetDataNotNull(
                    schema,
                    value,
                    () => FirePropertyChanging(schema),
                    () => FirePropertyChanged(schema));
            }
        }

        /// <summary>
        /// Initializes the mappings stored in the current instance.
        /// </summary>
        protected virtual void InitializeMappings()
        {
            PropertyObjectMappings.Add(BackEndIdPropertyName, "BackEndId");
            PropertyObjectMappings.Add(IsSynchronizedPropertyName, "IsSynchronized");
        }

        /// <summary>
        /// Checkes whether the IsSynchronized flag has changed between the
        /// call of the WatchForSynchronizedChange() method and the call of the
        /// IDispose.Dispose() method. If the IsSynchronized changed, the action
        /// callOnChange will be executed. 
        /// </summary>
        /// <param name="callOnChange">Specifies the action to execute if the IsSynchronized changed.</param>
        /// <returns>Returns an IDisposable object which can be used to stop the watch dog.</returns>
        protected IDisposable WatchForSynchronizedChange(Action callOnChange)
        {
            PreCondition.AssertNotNull(callOnChange, "callOnChange");

            bool isSynchronized = IsSynchronized;

            return new DisposeAction(() =>
            {
                if (isSynchronized != IsSynchronized)
                {
                    callOnChange();
                }
            });
        }

        /// <summary>
        /// Releases managed (c# or .net) resources.
        /// </summary>
        protected override void ReleaseManagedResources()
        {
            DisposeChildren();
            DisposeDataObject();

            Id = null;
            HashCode = 0;
            Context = null;
            _parent = null;
        }

        /// <summary>
        /// Fires after the dispoing changed event.
        /// </summary>
        protected override void FireDisposingChanged()
        {
            base.FireDisposingChanged();
            FirePropertyChanged("IsDisposing");
        }

        /// <summary>
        /// Fires after the dispose changed event.
        /// </summary>
        protected override void FireDisposedChanged()
        {
            base.FireDisposedChanged();
            FirePropertyChanged("IsDisposed");
        }

        /// <summary>
        /// Creates a new DomainSyncErrorInfo instance from the data of the given IDataSyncErrorInfo object.
        /// </summary>
        /// <param name="errorInfo">Specifies the error info provided by the underlying data layer.</param>
        /// <returns>Returns a new DomainSyncErrorInfo instance with additional error information.</returns>
        protected DomainSyncErrorInfo CreateDomainSyncErrorInfo(IDataSyncErrorInfo errorInfo)
        {
            return new DomainSyncErrorInfo
            {
                Error = errorInfo.Error,
                Field = TryGetFieldMeta(errorInfo.FieldMappedName),
                FieldMappedName = errorInfo.FieldMappedName,
                FieldDisplayName = errorInfo.FieldDisplayName,
                FieldErrorReason = EnumUtil.Convert<DataSyncErrorReason, DomainSyncErrorReason>(errorInfo.FieldErrorReason),
                OriginalValue = errorInfo.OriginalValue,
                Value = errorInfo.Value
            };
        }

        /// <summary>
        /// Evaluates the corresponding meta data object from the work item type.
        /// </summary>
        /// <param name="mappedName">Specifies the mapped name (from schema) which is used to retrieve the expected element.</param>
        /// <returns>Returns the expected meta field or a null pointer if it couldn't be found.</returns>
        protected virtual DomainMeta TryGetFieldMeta(string mappedName)
        {
            return null;
        }

        private void DisposeDataObject()
        {
            if (_dataObject != null)
            {
                // remove registrations
                _dataObject.PropertyChanging -= OnDataPropertyChanging;
                _dataObject.PropertyChanged -= OnDataPropertyChanged;
                _dataObject.Dispose();
                _dataObject = null;
            }            
        }

        private void DisposeChildren()
        {
            if (ChildCollections == null)
                return;

            ChildCollections.ForEach<IDomainCollection>(collection => collection.Dispose());
            ResetChildCollections();
        }

        private void PrepareDataLoad()
        {
            if (_updateState != UpdateState.None)
                throw new InvalidOperationException(Resources.ExcDmnObjAsyncCallbackPending);

            DisposeChildren();
        }

        private T SearchParent<T>()
            where T : DomainBase
        {
            DomainBase parent = this;
            while (parent != null)
            {
                if (parent is T)
                    return (T)parent;

                parent = parent.Parent;
            }
            return null;
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { _changedEvents.Add(EvtChanged, value); }
            remove { _changedEvents.Remove(EvtChanged, value); }
        }

        /// <summary>
        /// Occurs when a property value is changing.
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging
        {
            add { _changingEvents.Add(EvtChanging, value); }
            remove { _changingEvents.Remove(EvtChanging, value); }
        }

        /// <summary>
        /// Fires the PropertyChanged event.
        /// </summary>
        /// <param name="args">Event args to pass to the receivers.</param>
        protected void OnPropertyChanged(DomainChangedEventArgs args)
        {
            #region Important: refresh data cache with new back-end data
            
            if (args.PropertyName == BackEndIdPropertyName)
            {
                BackEndId = _dataObject.BackEndId;
            }
            if (args.PropertyName == IsSynchronizedPropertyName)
            {
                IsSynchronized = _dataObject.IsSynchronized;
            }
            
            #endregion

            // forward event
            _changedEvents.Fire(EvtChanged, this, args);

            if (ContextInternal != null)
            {
                ContextInternal.FireElementChanged(args);
            }
        }

        /// <summary>
        /// Fires the PropertyChanging event.
        /// </summary>
        /// <param name="args">Event args to pass to the receivers.</param>
        protected void OnPropertyChanging(DomainChangingEventArgs args)
        {
            _changingEvents.Fire(EvtChanging, this, args);

            if (ContextInternal != null)
            {
                ContextInternal.FireElementChanging(args);
            }
        }

        /// <summary>
        /// This method will be called if the data layer has changed a value
        /// on the current business object. The business object must notify
        /// the GUI/ViewModel components in order to update the users view.
        /// </summary>
        /// <param name="changedProperty">Specifies the property which has changed.</param>
        protected virtual void OnPropertyChangedByDataLayer(string changedProperty)
        {
            // 1. search in schema mappings for an appropriate property
            if (PropertySchemaMappings.ContainsSchema(changedProperty))
            {
                string domainPropName = PropertySchemaMappings[changedProperty];

                if (_stateHandler.ContainsKey(domainPropName))
                {
                    _stateHandler[domainPropName].Dispose();
                    _stateHandler.Remove(domainPropName);
                }

                OnPropertyChanged(new DomainChangedEventArgs(this, domainPropName));
            }
            // 2. search in dl to dm property mappings for an appropriate property
            else if (PropertyObjectMappings.ContainsProperty(changedProperty))
            {
                OnPropertyChanged(new DomainChangedEventArgs(this, PropertyObjectMappings[changedProperty]));
            }
        }

        /// <summary>
        /// This method will be called before the data layer has changed a value
        /// on the current business object. The business object must notify
        /// the GUI/ViewModel components in order to update the users view.
        /// </summary>
        /// <param name="changedProperty">Specifies the property which has changed.</param>
        protected virtual void OnPropertyChangingByDataLayer(string changedProperty)
        {
            // 1. search in schema mappings for an appropriate property
            if (PropertySchemaMappings.ContainsSchema(changedProperty))
            {
                string domainPropName = PropertySchemaMappings[changedProperty];

                _stateHandler[domainPropName] = new UndoStateHandler(
                    this,
                    DomainSchema[changedProperty],
                    PropertySchemaMappings.CanUndo);

                OnPropertyChanging(new DomainChangingEventArgs(this, domainPropName));
            }
            // 2. search in dl to dm property mappings for an appropriate property
            else if (PropertyObjectMappings.ContainsProperty(changedProperty))
            {
                OnPropertyChanging(new DomainChangingEventArgs(this, PropertyObjectMappings[changedProperty]));
            }
        }


        private void OnDataPropertyChanged(object sender, PropertyChangedEventArgs args)
        {
            if (Equals(sender, _dataObject))
            {
                OnPropertyChangedByDataLayer(args.PropertyName);
            }
        }

        private void OnDataPropertyChanging(object sender, PropertyChangingEventArgs args)
        {
            if (Equals(sender, _dataObject))
            {
                OnPropertyChangingByDataLayer(args.PropertyName);
            }
        }

        #endregion


        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        /// <summary>
        /// Enum required in order to save the domain collections update
        /// state.
        /// </summary>
        [Flags]
        private enum UpdateState
        {
            None = 0,
            CallbackPending = 1
        }



        /// <summary>
        /// Represents the internal callback handler which is used in order 
        /// to handle and fire the callback events.
        /// </summary>
        private class AsyncCallbackHandler
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------

            private readonly DomainBase _outer;
            private readonly AsyncCallback _callback;
            private readonly SchemaBase _storageSchema;
            private readonly ICollection _childInstances;

            private int _loadedCollections = 0;
            private bool _abort = false;

            #endregion

            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            internal AsyncCallbackHandler(DomainBase outer, AsyncCallback callback, SchemaBase storageSchema, ICollection childInstances)
            {
                PreCondition.AssertNotNull(outer, "outer");

                _outer = outer;
                _callback = callback;
                _storageSchema = storageSchema;
                _childInstances = childInstances;
            }

            #endregion

            #region Methods
            //--------------------------------------------------------------------
            // Methods
            //--------------------------------------------------------------------

            internal void InitDeep()
            {
                InitFlat(result =>
                {
                    EnsureContinue(result);
                    InitDeepWithoutCurrent();
                });
            }

            internal void InitDeepWithoutCurrent()
            {
                CheckLoadedElements(false);
                LoadDataOnCollections(true);                
            }

            internal void InitFlatWithoutCurrent()
            {
                CheckLoadedElements(false);
                LoadDataOnCollections(false);
            }

            internal void InitFlat()
            {
                InitFlat(_callback);
            }

            private void InitFlat(AsyncCallback callback)
            {
                _outer.ContextInternal.Data.Storage[_storageSchema].GetById(
                    _outer.ParentIdChain,
                    _outer.Id,
                    result =>
                        {
                            // IMPORTANT: Convert result from DataResult<IDataObject> to DataResult!!

                            if (!result.IsCompleted)
                            {
                                callback(new DataResult(((DataResult<IDataObject>)result).Error));
                            }
                            else
                            {
                                _outer.DataObject = ((DataResult<IDataObject>) result).Data;
                                callback(DataResult.Empty);
                            }
                        });
            }

            private void LoadDataOnCollections(bool deep)
            {
                if (_abort)
                    return;

                foreach (IDomainCollection collection in _childInstances)
                {
                    collection.LoadDataAsync(
                        deep,
                        result =>
                        {
                            EnsureContinue(result);
                            CheckLoadedElements(true);
                        });

                    if (_abort)
                        return;                    
                }
            }

            private void CheckLoadedElements(bool enlargeCounter)
            {
                if (_abort)
                    return;

                if (enlargeCounter)
                    ++_loadedCollections;

                if (_loadedCollections == _childInstances.Count)
                    _callback(DataResult.Empty);
            }

            private void EnsureContinue(IAsyncResult result)
            {
                if (!result.IsCompleted)
                {
                    _abort = true;
                    _callback(result);
                }
            }

            #endregion

            #region Events
            //--------------------------------------------------------------------
            // Events
            //--------------------------------------------------------------------

            #endregion
        }



        /// <summary>
        /// Represents the internal undo state handler which is used to add
        /// a new undo entry to the undo stack.
        /// </summary>
        private class UndoStateHandler : IDisposable
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------

            private readonly DomainBase _outer;
            private readonly IDataObjectState _stateBefore;
            private readonly SchemaBase _schema;
            private readonly Func<SchemaBase, object, object, bool> _canUndo;
            private readonly bool _useFullState;

            #endregion

            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            internal UndoStateHandler(DomainBase outer, SchemaBase schema, Func<SchemaBase, object, object, bool> canUndo, bool useFullState = false)
            {
                PreCondition.AssertNotNull(outer, "outer");
                PreCondition.AssertNotNull(schema, "schema");
                PreCondition.AssertNotNull(canUndo, "canUndo");

                _outer = outer;
                _useFullState = useFullState;
                _schema = schema;
                _canUndo = canUndo;
                _stateBefore = ExportState();
            }

            #endregion

            #region Methods
            //--------------------------------------------------------------------
            // Methods
            //--------------------------------------------------------------------

            /// <summary>
            /// Performs application-defined tasks associated with freeing,
            /// releasing, or resetting unmanaged resources.
            /// </summary>
            public void Dispose()
            {
                IDataObjectState stateAfter = ExportState();

                if (_canUndo(_schema, _stateBefore.State[_schema], stateAfter.State[_schema]))
                {
                    DomainMeta changedMetaField = _outer.TryGetFieldMeta(_schema.SchemaMappedName);

                    _outer.Context.UndoManager.AddEdit(
                        new DomainObjectUndoEditCommand(
                            _outer.Name,
                            _outer.TypedIdChain,
                            _stateBefore,
                            stateAfter,
                            _outer.PropertySchemaMappings[_schema], 
                            (changedMetaField != null) ? changedMetaField.DisplayName : null));
                }
            }

            private IDataObjectState ExportState()
            {
                if (_useFullState)
                {
                    return _outer.DataObject.ExportState();
                }
                return _outer.DataObject.ExportState(_schema);
            }

            #endregion

            #region Events
            //--------------------------------------------------------------------
            // Events
            //--------------------------------------------------------------------

            #endregion
        }
    }
}